﻿// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.CodeAnalysis;
using StreamJsonRpc.Analyzers;

namespace StreamJsonRpc.Analyzers.GeneratorModels;

internal record FullModel
{
    internal FullModel(ImmutableEquatableSet<ProxyModel> proxies, ImmutableEquatableArray<AttachUse> attachUses, ImmutableEquatableSet<InterfaceModel> optionalInterfacesOrTheirPrimaries, bool interceptorsEnabled)
    {
        // Generate a proxy for attributed interfaces in this assembly, and for interfaces used by Attach methods.
        this.Proxies = [.. proxies.Concat(attachUses.Where(a => a.Contracts is not null).Select(a => new ProxyModel(a.Contracts!, a.ExternalProxyName))).Distinct()];
        this.OptionalInterfacesOrTheirPrimaries = optionalInterfacesOrTheirPrimaries;

        if (interceptorsEnabled)
        {
            this.Interceptions = [..
                from use in attachUses
                group use by (use.Contracts, use.Signature, use.ExternalProxyName) into attachByProxy
                let proxy = attachByProxy.Key.Contracts is null ? null : new ProxyModel(attachByProxy.Key.Contracts, attachByProxy.Key.ExternalProxyName)
                select new InterceptionModel(proxy, attachByProxy.Key.Signature, [.. from attach in attachByProxy select attach.InterceptableLocation])];
        }
    }

    internal required bool PublicRpcMarshalableInterfaceExtensions { get; init; }

    internal ImmutableEquatableArray<ProxyModel> Proxies { get; }

    internal ImmutableEquatableSet<InterfaceModel> OptionalInterfacesOrTheirPrimaries { get; }

    internal ImmutableEquatableArray<InterceptionModel> Interceptions { get; } = [];

    internal required bool PublicProxies { get; init; }

    internal void GenerateSource(SourceProductionContext context)
    {
        try
        {
            foreach (ProxyModel proxy in this.Proxies)
            {
                proxy.GenerateSource(context, this.PublicProxies);
            }

            if (this.Interceptions is not [])
            {
                this.GenerateInterceptor(context, this.Interceptions);
            }

            this.GenerateOptionalInterfaceExtensionMethods(context);
        }
        catch (Exception) when (AnalyzerUtilities.LaunchDebugger())
        {
            throw;
        }
    }

    private void GenerateOptionalInterfaceExtensionMethods(SourceProductionContext context)
    {
        SourceWriter writer = new();
        writer.WriteLine("""
            // <auto-generated/>

            #nullable enable
            #pragma warning disable CS0436 // prefer local types to imported ones
            #pragma warning disable // Disable all warnings so that [Experimental] APIs don't flag anything.

            using StreamJsonRpc;

            """);

        IEnumerable<IGrouping<Container?, InterfaceModel>> interfacesByNamespace =
            from iface in this.OptionalInterfacesOrTheirPrimaries
            where iface.DeclaredInThisCompilation && iface.IsFullyPartial
            group iface by iface.Container?.ThisOrContainingNamespace into ifaceGroup
            select ifaceGroup;

        bool nonEmptyFile = false;
        foreach (IGrouping<Container?, InterfaceModel> ifaceGroup in interfacesByNamespace)
        {
            if (ifaceGroup.Key is null)
            {
                WriteGroup(writer);
            }
            else
            {
                ifaceGroup.Key.WriteWithin(writer, writer => WriteGroup(writer));
            }

            void WriteGroup(SourceWriter writer)
            {
                bool publicClass = this.PublicRpcMarshalableInterfaceExtensions && ifaceGroup.Any(i => i.IsPublic);
                writer.WriteLine($$"""
                    /// <summary>Extension methods for interfaces acting as optional interfaces on proxies.</summary>
                    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{ThisAssembly.AssemblyName}}", "{{ThisAssembly.AssemblyFileVersion}}")]
                    """);

                if (publicClass)
                {
                    writer.WriteLine("""
                        [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
                        """);
                }

                writer.WriteLine($$"""
                    {{(publicClass ? "public" : "internal")}} static partial class StreamJsonRpcOptionalInterfaceAccessors
                    {
                    """);
                writer.Indentation++;

                bool first = true;
                foreach (InterfaceModel iface in ifaceGroup)
                {
                    string visibility = iface.IsPublic ? "public" : "internal";
                    writer.WriteLine($$"""
                        /// <inheritdoc cref="global::StreamJsonRpc.IClientProxy.Is(global::System.Type)"/>
                        {{visibility}} static bool Is(this {{iface.FullName}}? self, global::System.Type type) => self switch { null => false, global::StreamJsonRpc.IClientProxy proxy => proxy.Is(type), _ => type.IsAssignableFrom(self.GetType()) };
                        /// <inheritdoc cref="global::StreamJsonRpc.JsonRpcExtensions.As{T}(global::StreamJsonRpc.IClientProxy)"/>
                        {{visibility}} static T? As<T>(this {{iface.FullName}}? self) where T : class => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.As<T>() : self as T;
                        """);
                    if (first)
                    {
                        writer.WriteLine();
                        first = false;
                    }

                    nonEmptyFile = true;
                }

                writer.Indentation--;
                writer.WriteLine("}");
            }
        }

        if (nonEmptyFile)
        {
            context.AddSource("OptionalInterfaceExtensions.g.cs", writer.ToSourceText());
        }
    }

    private void GenerateInterceptor(SourceProductionContext context, ImmutableEquatableArray<InterceptionModel> interceptions)
    {
        SourceWriter writer = new();
        writer.WriteLine($$"""
            // <auto-generated/>

            #nullable enable

            namespace System.Runtime.CompilerServices
            {
            #pragma warning disable CS9113
                [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
                file sealed class InterceptsLocationAttribute(int version, string data) : Attribute
                {
                }
            #pragma warning restore CS9113
            }

            #pragma warning disable CS0436 // prefer local types to imported ones

            namespace {{ProxyGenerator.GenerationNamespace}}
            {
            """);

        writer.Indentation++;
        writer.WriteLine($$"""
            [global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{ThisAssembly.AssemblyName}}", "{{ThisAssembly.AssemblyFileVersion}}")]
            file static class StreamJsonRpcInterceptor
            {
            """);

        writer.Indentation++;
        foreach (InterceptionModel model in interceptions)
        {
            model.WriteInterceptor(writer);
        }

        writer.WriteLine("""

            private static global::StreamJsonRpc.IJsonRpcClientProxy StartListening(global::StreamJsonRpc.IJsonRpcClientProxy proxy)
            {
                proxy.JsonRpc.StartListening();
                return proxy;
            }
            """);

        writer.Indentation--;
        writer.WriteLine($$"""
            }
            """);
        writer.Indentation--;

        writer.WriteLine($$"""
            }
            """);

        context.AddSource("Interceptor.g.cs", writer.ToSourceText());
    }
}
